Trước tiên, chúng ta hãy cùng cafedev xem xét kịch bản sau để hiểu mô hình quan sát(observer pattern). Ace có thể tham khảo thêm các bài khác tại series Design Pattern tại đây.
Nội dung chính
1. Tình huống :
Giả sử chúng ta đang xây dựng một ứng dụng cricket thông báo cho người xem về các thông tin như điểm số hiện tại, tỷ lệ chạy, v.v. Giả sử chúng ta đã tạo hai phần tử hiển thị là CurrentScoreDisplay và AverageScoreDisplay. CricketData có tất cả dữ liệu (runs, bowls, v.v.) và bất cứ khi nào dữ liệu thay đổi, các phần tử hiển thị sẽ được thông báo với dữ liệu mới và chúng hiển thị dữ liệu mới nhất tương ứng.
Dưới đây là triển khai code java của thiết kế này.
// Java implementation of above design for Cricket App. The
// problems with this design are discussed below.
// A class that gets information from stadium and notifies
// display elements, CurrentScoreDisplay & AverageScoreDisplay
class CricketData
{
int runs, wickets;
float overs;
CurrentScoreDisplay currentScoreDisplay;
AverageScoreDisplay averageScoreDisplay;
// Constructor
public CricketData(CurrentScoreDisplay currentScoreDisplay,
AverageScoreDisplay averageScoreDisplay)
{
this.currentScoreDisplay = currentScoreDisplay;
this.averageScoreDisplay = averageScoreDisplay;
}
// Get latest runs from stadium
private int getLatestRuns()
{
// return 90 for simplicity
return 90;
}
// Get latest wickets from stadium
private int getLatestWickets()
{
// return 2 for simplicity
return 2;
}
// Get latest overs from stadium
private float getLatestOvers()
{
// return 10.2 for simplicity
return (float)10.2;
}
// This method is used update displays when data changes
public void dataChanged()
{
// get latest data
runs = getLatestRuns();
wickets = getLatestWickets();
overs = getLatestOvers();
currentScoreDisplay.update(runs,wickets,overs);
averageScoreDisplay.update(runs,wickets,overs);
}
}
// A class to display average score. Data of this class is
// updated by CricketData
class AverageScoreDisplay
{
private float runRate;
private int predictedScore;
public void update(int runs, int wickets, float overs)
{
this.runRate = (float)runs/overs;
this.predictedScore = (int) (this.runRate * 50);
display();
}
public void display()
{
System.out.println("\nAverage Score Display:\n" +
"Run Rate: " + runRate +
"\nPredictedScore: " + predictedScore);
}
}
// A class to display score. Data of this class is
// updated by CricketData
class CurrentScoreDisplay
{
private int runs, wickets;
private float overs;
public void update(int runs,int wickets,float overs)
{
this.runs = runs;
this.wickets = wickets;
this.overs = overs;
display();
}
public void display()
{
System.out.println("\nCurrent Score Display: \n" +
"Runs: " + runs +"\nWickets:"
+ wickets + "\nOvers: " + overs );
}
}
// Driver class
class Main
{
public static void main(String args[])
{
// Create objects for testing
AverageScoreDisplay averageScoreDisplay =
new AverageScoreDisplay();
CurrentScoreDisplay currentScoreDisplay =
new CurrentScoreDisplay();
// Pass the displays to Cricket data
CricketData cricketData = new CricketData(currentScoreDisplay,
averageScoreDisplay);
// In real app you would have some logic to call this
// function when data changes
cricketData.dataChanged();
}
}
Đầu ra:
Current Score Display:
Runs: 90
Wickets:2
Overs: 10.2
Average Score Display:
Run Rate: 8.823529
PredictedScore: 441
2. Các vấn đề với thiết kế trên:
- CricketData giữ các tham chiếu đến các đối tượng phần tử hiển thị cụ thể mặc dù nó chỉ cần gọi phương thức cập nhật của các đối tượng này. Nó có quyền truy cập vào quá nhiều thông tin bổ sung so với yêu cầu.
- Câu lệnh này “currentScoreDisplay.update (running, wickets, overs);” vi phạm một trong những nguyên tắc thiết kế quan trọng nhất “Chương trình với giao diện, không phải triển khai.” vì chúng ta đang sử dụng các đối tượng cụ thể để chia sẻ dữ liệu hơn là các giao diện trừu tượng.
- CricketData và các yếu tố hiển thị được kết hợp chặt chẽ với nhau.
- Nếu trong tương lai có một yêu cầu khác và chúng ta cần thêm phần tử hiển thị khác, chúng ta cần thực hiện các thay đổi đối với phần không thay đổi trong code của chúng ta (CricketData). Đây chắc chắn không phải là một phương pháp thiết kế tốt và ứng dụng có thể không xử lý được các thay đổi và không dễ bảo trì.
Làm thế nào để tránh những vấn đề này?
Sử dụng Observer Pattern
Observer pattern
Để hiểu Observer pattern, trước tiên bạn cần hiểu chủ thể và đối tượng quan sát.
Mối quan hệ giữa chủ thể và người quan sát có thể dễ dàng được hiểu như một sự tương đồng với việc đăng ký tạp chí.
- Một nhà xuất bản tạp chí đang kinh doanh và xuất bản tạp chí (dữ liệu).
- Nếu bạn (người sử dụng dữ liệu / người quan sát) quan tâm đến tạp chí mà bạn đăng ký và nếu một ấn bản mới được xuất bản, nó sẽ được giao cho bạn.
- Nếu bạn hủy đăng ký, bạn sẽ ngừng nhận các ấn bản mới.
- Nhà xuất bản không biết bạn là ai và bạn sử dụng tạp chí như thế nào, họ chỉ giao nó cho bạn vì bạn là người đăng ký (khớp nối lỏng lẻo).
3. Định nghĩa về Observer Pattern:
Observer Pattern định nghĩa một sự phụ thuộc từ một đến nhiều giữa các đối tượng để một đối tượng thay đổi trạng thái, tất cả các phụ thuộc của nó được thông báo và cập nhật tự động.
Giải trình:
- Sự phụ thuộc một đến nhiều là giữa Chủ thể(Subject) (Một) và Người quan sát(Observer) (Nhiều).
- Có sự phụ thuộc vì bản thân Người quan sát(Observer) không có quyền truy cập vào dữ liệu. Họ phụ thuộc vào Chủ thể(Subject) để cung cấp dữ liệu cho họ.
Sơ đồ lớp:
Nguồn hình ảnh: Wikipedia
- Ở đây Observer và Subject là các giao diện (có thể là bất kỳ kiểu siêu trừu tượng nào không nhất thiết là giao diện java).
- Tất cả những người quan sát(Observer) cần dữ liệu phải triển khai observer interface.
- Phương thức notify() trong observer interface định nghĩa hành động được thực hiện khi chủ thể(Subject) cung cấp dữ liệu cho nó.
- Đối tượng duy trì một observerCollection là danh sách các quan sát viên hiện đã đăng ký (đã đăng ký).
- registerObserver (người quan sát) và unregisterObserver (người huỷ quan sát) là các phương thức để thêm và bớt người quan sát tương ứng.
- tifyObservers() được gọi khi dữ liệu được thay đổi và các quan sát viên cần được cung cấp dữ liệu mới.
4. Ưu điểm của Observer pattern:
Cung cấp một thiết kế liên kết lỏng lẻo giữa các đối tượng tương tác. Các đối tượng liên kết lỏng lẻo linh hoạt với các yêu cầu thay đổi. Ở đây, khớp nối lỏng lẻo có nghĩa là các đối tượng tương tác nên có ít thông tin về nhau hơn.
Mô hình trình quan sát cung cấp khớp nối lỏng lẻo này như:
- Đối tượng chỉ biết rằng người quan sát thực hiện Observer interface. Không có gì thêm.
- Không cần phải sửa đổi Chủ đề(Subject) để thêm hoặc bớt người quan sát.
- Chúng ta có thể sử dụng lại các lớp chủ thể(Subject) và lớp quan sát độc lập với nhau.
Nhược điểm:
- Rò rỉ bộ nhớ gây ra bởi sự cố vì đăng ký lắng nghe thì nhiều và rõ ràng nhưng việc hủy người đăng ký thì không có, dẫn tới nhiều chỗ không còn dùng tới việc đăng ký mà vẫn đang kết nối với Chủ đề.
5. Khi nào sử dụng pattern này?
Bạn nên cân nhắc sử dụng pattern này trong ứng dụng của mình khi nhiều đối tượng phụ thuộc vào trạng thái của một đối tượng vì nó cung cấp một thiết kế gọn gàng và được kiểm tra tốt cho cùng một đối tượng.
Sử dụng trong cuộc sống thực:
- Nó được sử dụng nhiều trong bộ công cụ GUI và trình nghe sự kiện. Trong java, button (chủ đề) và onClickListener (người quan sát) được mô hình hóa với observer pattern.
- Phương tiện truyền thông xã hội, nguồn cấp dữ liệu RSS, đăng ký email trong đó bạn có tùy chọn theo dõi hoặc đăng ký và bạn nhận được thông báo mới nhất.
- Tất cả người dùng ứng dụng trên cửa hàng Play đều được thông báo nếu có bản cập nhật.
Cài ứng dụng cafedev để dễ dàng cập nhật tin và học lập trình mọi lúc mọi nơi tại đây.
Tài liệu từ cafedev:
- Full series tự học Design Pattern từ cơ bản tới nâng cao tại đây nha.
- Các nguồn kiến thức MIỄN PHÍ VÔ GIÁ từ cafedev tại đây
Nếu bạn thấy hay và hữu ích, bạn có thể tham gia các kênh sau của cafedev để nhận được nhiều hơn nữa:
Chào thân ái và quyết thắng!